# fastmcp_server.py  – 增强版（带错误回执与详细 docstring）
from __future__ import annotations
import logging, os, re, json, time, requests, functools, traceback
from typing import List, Dict, Any, Callable
from fastmcp import FastMCP
from mcp.types import TextContent
from logging.handlers import TimedRotatingFileHandler

# ---------------- 1. 日志配置 -----------------
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
LOG_FMT = "%(asctime)s | %(levelname)8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s"
os.makedirs("logs", exist_ok=True)
file_handler = TimedRotatingFileHandler(
    filename="logs/mcp-weather-server.log",
    when="midnight", interval=1, backupCount=14, encoding="utf-8"
)
file_handler.setFormatter(logging.Formatter(LOG_FMT))
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(LOG_FMT))
logging.basicConfig(level=LOG_LEVEL, handlers=[console_handler, file_handler], force=True)
logger = logging.getLogger("mcp-weather-server")

# ---------------- 2. FastMCP 实例 -----------------
mcp = FastMCP("weather")

# ---------------- 3. 辅助函数 -----------------
def return_error(func: Callable) -> Callable:
    """异常回传装饰器：捕获函数异常并返回 types.TextContent 错误信息"""
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except Exception as e:
            logger.error("[%s] 异常: %s\n%s", func.__name__, e, traceback.format_exc())
            return [TextContent(type="text", text=f"错误：{e}")]
    return wrapper

def _validate_location(location: str) -> None:
    """验证位置参数是否有效"""
    if not location or not location.strip():
        raise ValueError("位置参数不能为空")
    if len(location.strip()) < 2:
        raise ValueError("位置参数至少需要2个字符")

# ---------------- 3. 业务函数 -----------------
CITY_LIST = ["北京", "上海", "天津", "重庆", "黑龙江", "吉林", "辽宁", "内蒙古",
             "河北", "山西", "陕西", "山东", "新疆", "西藏", "青海", "甘肃", "宁夏",
             "河南", "江苏", "湖北", "浙江", "安徽", "福建", "江西", "湖南", "贵州",
             "四川", "广东", "云南", "广西", "海南"]

def get_city_list() -> List[Dict[str, str]]:
    """拉取并解析天气网城市列表，返回统一结构 List[Dict]"""
    logger.info("[get_city_list] 开始拉取城市列表")
    url = "https://toy1.weather.com.cn/search"
    headers = {
        "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0"),
        "Referer": "https://www.weather.com.cn/",
    }
    parsed: list[dict[str, str]] = []
    for city_name in CITY_LIST:
        params = {
            "cityname": city_name,
            "callback": f"jQuery{int(time.time()*1000)}_{int(time.time()*1000)}",
            "_": int(time.time()*1000),
        }
        try:
            r = requests.get(url, params=params, headers=headers, timeout=10)
            r.raise_for_status()
            json_str = re.sub(r'^jQuery\d+_\d+\(|\)$', '', r.text)
            for item in json.loads(json_str):
                ref = item["ref"].split("~")
                parsed.append({
                    "city_id": ref[0],
                    "city_cn": ref[9],
                    "city_en": ref[1],
                    "district_cn": ref[2],
                    "district_en": ref[3],
                })
        except Exception as exc:
            logger.warning("[get_city_list] 拉取 %s 失败: %s", city_name, exc)
    logger.debug("[get_city_list] 拉取到的原始数据: %s", parsed)
    logger.info("[get_city_list] 共获取 %d 条城市记录", len(parsed))
    return parsed

def get_weather_by_cityId(city_id: str) -> List[Dict[str, Any]]:
    """根据城市 ID 获取 7 天天气预报"""
    logger.info("[get_weather_by_cityId] city_id=%s", city_id)
    url = f"https://www.weather.com.cn/weather/{city_id}.shtml"
    headers = {
        "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0"),
        "Referer": "https://www.weather.com.cn/",
    }
    try:
        r = requests.get(url, headers=headers, timeout=10)
        r.encoding = "utf-8"
        m = re.search(r'var hour3data=({.*?});?', r.text, re.DOTALL)
        if not m:
            logger.warning("[get_weather_by_cityId] 未匹配到天气数据, city_id=%s", city_id)
            return []
        data = json.loads(m.group(1))
        logger.debug("[get_weather_by_cityId] 原始数据: %s", str(data)[:300])
        result = data.get("7d", [])
        logger.debug("[get_weather_by_cityId] 7d数据类型: %s, 内容: %s", type(result), str(result)[:200])
        return result
    except Exception as exc:
        logger.error("[get_weather_by_cityId] 请求失败, city_id=%s: %s", city_id, exc)
        return []

def parse_location(input_str: str) -> tuple[str, str]:
    """将用户输入解析为 (city, district)"""
    logger.debug("[parse_location] 输入: %s", input_str)
    cleaned = re.sub(r'[、，,/|]', ' ', input_str)
    cleaned = re.sub(r'市|区', '', cleaned)
    cleaned = re.sub(r'\s+', ' ', cleaned).strip()
    parts = cleaned.split(' ', 1)
    if len(parts) == 2:
        city, district = parts
    elif len(parts) == 1:
        city = district = cleaned
    elif len(cleaned) > 2:
        city, district = cleaned[:2], cleaned[2:]
    else:
        city, district = cleaned, ''
    logger.debug("[parse_location] 解析结果 city=%s district=%s", city, district)
    return city, district

# ---------------- 4. 工具注册 -----------------
@mcp.tool()
@return_error
async def get_weather(input_str: str) -> List[TextContent]:
    """
    功能：查询指定城市/区域未来 7 天的天气预报。

    参数：
        input_str (str): 用户输入的位置描述，支持「城市」「城市 区/县」两种形式。
                        例如：「北京」「北京 海淀」「杭州 西湖」。

    返回：
        List[types.ContentBlock]: 返回天气信息的纯文本内容列表
    """
    logger.info("[get_weather] 收到请求 input_str=%s", input_str)
    _validate_location(input_str)
    
    city, district = parse_location(input_str)
    city_list = get_city_list()

    matched = [item for item in city_list
               if item["city_cn"] == city and item["district_cn"] == district]
    if not matched:
        logger.warning("[get_weather] 未找到城市: %s-%s", city, district)
        raise ValueError(f"暂不支持查询「{input_str}」的天气，请检查城市/区县名称或先调用 list_cities 查看支持列表")

    city_id = matched[0]["city_id"]
    weather = get_weather_by_cityId(city_id)

    if not weather:
        raise ValueError("天气接口返回为空，请稍后重试")
    
    logger.info("[get_weather] 成功返回 %d 条天气记录", len(weather))
    logger.debug("[get_weather] 天气数据类型: %s, 内容: %s", type(weather), str(weather)[:200])
    
    # 格式化天气信息为纯文本
    weather_text = f"{input_str} 未来7天天气预报：\n\n"
    for i, day in enumerate(weather):
        # day 是一个列表，包含多个时间点的天气数据
        if isinstance(day, list) and len(day) > 0:
            # 取第一个时间点的数据作为当天的代表
            first_hour = day[0]
            # first_hour 是字符串格式："23日11时,d02,阴,12℃,东北风,<3级,3"
            parts = first_hour.split(',')
            if len(parts) >= 4:
                date = parts[0]  # 23日11时
                weather_desc = parts[2]  # 阴
                temp = parts[3]  # 12℃
                weather_text += f"第{i+1}天 ({date}): {weather_desc}，温度 {temp}\n"
            else:
                weather_text += f"第{i+1}天: {first_hour}\n"
        else:
            weather_text += f"第{i+1}天: {str(day)}\n"
    
    return [TextContent(type="text", text=weather_text)]

@mcp.tool()
@return_error
async def list_cities() -> List[TextContent]:
    """
    功能：返回所有支持天气查询的城市/区县列表，供用户选择。

    参数：无

    返回：
        List[types.ContentBlock]: 返回城市列表的纯文本内容
    """
    logger.info("[list_cities] 收到请求")
    cities = get_city_list()
    logger.info("[list_cities] 返回 %d 条城市记录", len(cities))
    
    # 格式化城市列表为纯文本
    cities_text = "支持天气查询的城市列表：\n\n"
    for city in cities:
        cities_text += f"{city.get('city_cn', '未知')} - {city.get('district_cn', '未知')} (ID: {city.get('city_id', '未知')})\n"
    
    return [TextContent(type="text", text=cities_text)]

# ---------------- 5. 启动入口 -----------------
if __name__ == "__main__":
    logger.info("Starting mcp-weather-server on 0.0.0.0:7861")
    try:
        mcp.run(transport="streamable-http", host="0.0.0.0", port=7861)
    except TypeError:   # 旧版 fastmcp 回退
        import uvicorn
        uvicorn.run("fastmcp_server:mcp", host="0.0.0.0", port=7861)